D:\a\cssh-rs\cssh-rs\cssh-rs-platform-windows\src\traits.rs
Line | Count | Source |
1 | | //! Windows implementations of the `cssh-rs-platform` traits. |
2 | | //! |
3 | | //! The impls wrap the existing [`WindowsApi`] helpers and |
4 | | //! `tokio::net::windows::named_pipe` so the daemon and client can be |
5 | | //! ported to the platform-agnostic surface in later milestones. They are |
6 | | //! not consumed by the M0 daemon/client yet; the trait surface is what |
7 | | //! Linux and macOS stubs need to mirror. |
8 | | //! |
9 | | //! [`WindowsApi`]: crate::api::WindowsApi |
10 | | |
11 | | use std::ffi::{OsStr, OsString}; |
12 | | use std::io; |
13 | | use std::sync::Arc; |
14 | | |
15 | | use cssh_rs_platform::{ |
16 | | ControlChannelClient, ControlChannelServer, LaunchContext, ProcessSpawner, WindowHandleProbe, |
17 | | }; |
18 | | use tokio::io::{AsyncReadExt, AsyncWriteExt, Interest}; |
19 | | use tokio::net::windows::named_pipe::{ |
20 | | ClientOptions, NamedPipeClient, NamedPipeServer, PipeMode, ServerOptions, |
21 | | }; |
22 | | use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND}; |
23 | | use windows::Win32::System::Threading::PROCESS_INFORMATION; |
24 | | |
25 | | use crate::api::{DefaultWindowsApi, WindowsApi}; |
26 | | |
27 | | /// `Send + Sync` wrapper around [`PROCESS_INFORMATION`]. |
28 | | /// |
29 | | /// [`PROCESS_INFORMATION`] is `!Send` because its `HANDLE` fields wrap raw |
30 | | /// pointers. The handles are owned by the spawning process and remain valid |
31 | | /// across thread boundaries, so the wrapper asserts thread safety with |
32 | | /// `unsafe impl`. The daemon already uses the same pattern for |
33 | | /// `Client::process_handle` via its private `HWNDWrapper`. |
34 | | #[derive(Debug)] |
35 | | pub struct SendProcessInformation(pub PROCESS_INFORMATION); |
36 | | |
37 | | // SAFETY: PROCESS_INFORMATION's HANDLE fields are opaque kernel handles, |
38 | | // not pointers into the calling thread's address space; the OS allows the |
39 | | // owning process to use them from any thread. |
40 | | unsafe impl Send for SendProcessInformation {} |
41 | | // SAFETY: same justification as for `Send` - the handle values are immutable |
42 | | // once `CreateProcessW` returns, so shared references can travel freely. |
43 | | unsafe impl Sync for SendProcessInformation {} |
44 | | |
45 | | impl Drop for SendProcessInformation { |
46 | 3 | fn drop(&mut self) { |
47 | | // CreateProcessW returns kernel handles to the new process and its |
48 | | // primary thread; both must be closed to avoid leaking entries from |
49 | | // the per-process handle table. Skip NULL/invalid handles to avoid |
50 | | // the ERROR_INVALID_HANDLE that CloseHandle reports for them - see |
51 | | // https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle |
52 | 6 | for handle in [self.0.hProcess, self.0.hThread]3 { |
53 | 6 | if !handle.is_invalid() { |
54 | 0 | // SAFETY: handle came from CreateProcessW (or a test-side |
55 | 0 | // fake) and is not aliased anywhere else - the wrapper owns |
56 | 0 | // it for its entire lifetime. |
57 | 0 | let _ = unsafe { CloseHandle(handle) }; |
58 | 6 | } |
59 | | } |
60 | 3 | self.0.hProcess = HANDLE::default(); |
61 | 3 | self.0.hThread = HANDLE::default(); |
62 | 3 | } |
63 | | } |
64 | | |
65 | | /// `Send + Sync` wrapper around [`HWND`]. |
66 | | /// |
67 | | /// Mirrors the daemon's private `HWNDWrapper` so the platform-trait |
68 | | /// surface can be `Send + Sync` without consumers having to define their |
69 | | /// own newtype. |
70 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
71 | | pub struct SendHwnd(pub HWND); |
72 | | |
73 | | // SAFETY: HWND is an opaque user-mode kernel object identifier the OS lets |
74 | | // any thread in the owning process consult. |
75 | | unsafe impl Send for SendHwnd {} |
76 | | // SAFETY: same justification as for `Send` - the underlying identifier is |
77 | | // immutable and read-only from Rust's perspective. |
78 | | unsafe impl Sync for SendHwnd {} |
79 | | |
80 | | /// Windows-side [`LaunchContext`]. |
81 | | /// |
82 | | /// Carries the focus-handoff hint the daemon uses when spawning client |
83 | | /// consoles. The current Win32 spawn path inspects the same flag to apply |
84 | | /// `STARTF_USESHOWWINDOW` + `SW_SHOWNOACTIVATE`, so this field is the |
85 | | /// minimum needed to round-trip today's behaviour through the trait. |
86 | | #[derive(Debug, Clone, Copy, Default)] |
87 | | pub struct WindowsLaunchContext { |
88 | | /// Whether the spawned console is allowed to take foreground focus. |
89 | | pub with_keyboard_focus: bool, |
90 | | } |
91 | | |
92 | | impl LaunchContext for WindowsLaunchContext {} |
93 | | |
94 | | /// Process spawner backed by [`WindowsApi::create_process_with_os_args`]. |
95 | | /// |
96 | | /// Holds an [`Arc`] over a [`WindowsApi`] implementation so callers can |
97 | | /// inject a mock in tests (the crate's `mock` feature exposes |
98 | | /// `MockWindowsApi` for this purpose); production code uses |
99 | | /// [`Self::default`] which selects [`DefaultWindowsApi`]. |
100 | | pub struct WindowsProcessSpawner<A: WindowsApi = DefaultWindowsApi> { |
101 | | api: Arc<A>, |
102 | | } |
103 | | |
104 | | impl<A: WindowsApi> WindowsProcessSpawner<A> { |
105 | | /// Construct a spawner around a specific [`WindowsApi`] implementation. |
106 | | /// |
107 | | /// # Arguments |
108 | | /// * `api` - Windows API operations implementation. |
109 | 4 | pub fn new(api: Arc<A>) -> Self { |
110 | 4 | return Self { api }; |
111 | 4 | } |
112 | | } |
113 | | |
114 | | impl Default for WindowsProcessSpawner<DefaultWindowsApi> { |
115 | 1 | fn default() -> Self { |
116 | 1 | return Self::new(Arc::new(DefaultWindowsApi)); |
117 | 1 | } |
118 | | } |
119 | | |
120 | | impl<A: WindowsApi + 'static> ProcessSpawner for WindowsProcessSpawner<A> { |
121 | | type Context = WindowsLaunchContext; |
122 | | type Handle = SendProcessInformation; |
123 | | type Error = io::Error; |
124 | | |
125 | 3 | fn spawn( |
126 | 3 | &self, |
127 | 3 | program: &OsStr, |
128 | 3 | args: &[OsString], |
129 | 3 | context: &Self::Context, |
130 | 3 | ) -> Result<Self::Handle, Self::Error> { |
131 | 3 | return self |
132 | 3 | .api |
133 | 3 | .create_process_with_os_args(program, args, context.with_keyboard_focus) |
134 | 3 | .map(SendProcessInformation) |
135 | 3 | .map_err(|err| {1 |
136 | | // The thread-local last-error code is what CreateProcessW |
137 | | // actually reports; the windows-rs `Error` typically just |
138 | | // re-wraps it but stringifies it as an opaque HRESULT. |
139 | 1 | let os_err = io::Error::last_os_error(); |
140 | 1 | if os_err.raw_os_error().unwrap_or(0) != 0 { |
141 | 1 | return os_err; |
142 | 0 | } |
143 | 0 | return io::Error::other(format!("CreateProcessW failed: {err}")); |
144 | 1 | }); |
145 | 3 | } |
146 | | } |
147 | | |
148 | | /// Daemon-side control channel backed by a Windows named pipe server. |
149 | | /// |
150 | | /// One instance owns one [`NamedPipeServer`] endpoint. `accept` |
151 | | /// waits for the matching client process to connect; `send` / `recv` |
152 | | /// then exchange already-framed bytes produced by `cssh-rs-protocol`. |
153 | | pub struct WindowsControlChannelServer { |
154 | | pipe: NamedPipeServer, |
155 | | endpoint: OsString, |
156 | | } |
157 | | |
158 | | impl WindowsControlChannelServer { |
159 | | /// Open a new named-pipe server endpoint at `endpoint`. |
160 | | /// |
161 | | /// # Arguments |
162 | | /// * `endpoint` - Fully-qualified Windows named-pipe name |
163 | | /// (e.g. `\\.\pipe\cssh-rs-named-pipe-for-ipc`). |
164 | | /// |
165 | | /// # Returns |
166 | | /// Ready-to-`accept` server, or the underlying I/O error from |
167 | | /// [`ServerOptions::create`]. |
168 | 1 | pub fn bind(endpoint: &OsStr) -> io::Result<Self> { |
169 | 1 | let pipe = ServerOptions::new() |
170 | 1 | .access_inbound(true) |
171 | 1 | .access_outbound(true) |
172 | 1 | .pipe_mode(PipeMode::Message) |
173 | 1 | .create(endpoint)?0 ; |
174 | 1 | return Ok(Self { |
175 | 1 | pipe, |
176 | 1 | endpoint: endpoint.to_owned(), |
177 | 1 | }); |
178 | 1 | } |
179 | | |
180 | | /// Endpoint name the server is bound to. |
181 | | /// |
182 | | /// # Returns |
183 | | /// The named-pipe path supplied to [`Self::bind`]. |
184 | 1 | pub fn endpoint(&self) -> &OsStr { |
185 | 1 | return self.endpoint.as_os_str(); |
186 | 1 | } |
187 | | |
188 | | /// Test for pending readable bytes without blocking. |
189 | | /// |
190 | | /// # Returns |
191 | | /// `Ok(true)` when the pipe has bytes ready to read, `Ok(false)` |
192 | | /// otherwise, or the underlying I/O error. |
193 | 1 | pub async fn ready_to_read(&mut self) -> io::Result<bool> { |
194 | 1 | let ready = self.pipe.ready(Interest::READABLE).await?0 ; |
195 | 1 | return Ok(ready.is_readable()); |
196 | 1 | } |
197 | | } |
198 | | |
199 | | impl ControlChannelServer for WindowsControlChannelServer { |
200 | | type Error = io::Error; |
201 | | |
202 | 1 | async fn accept(&mut self) -> Result<(), Self::Error> { |
203 | 1 | return self.pipe.connect().await; |
204 | 1 | } |
205 | | |
206 | 1 | async fn send(&mut self, frame: &[u8]) -> Result<(), Self::Error> { |
207 | 1 | return self.pipe.write_all(frame).await; |
208 | 1 | } |
209 | | |
210 | 1 | async fn recv(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> { |
211 | 1 | return self.pipe.read(buf).await; |
212 | 1 | } |
213 | | } |
214 | | |
215 | | /// Client-side control channel backed by a Windows named pipe client. |
216 | | pub struct WindowsControlChannelClient { |
217 | | pipe: Option<NamedPipeClient>, |
218 | | } |
219 | | |
220 | | impl WindowsControlChannelClient { |
221 | | /// Construct an unconnected control-channel client. |
222 | | /// |
223 | | /// # Returns |
224 | | /// A client whose `connect` has not yet run. |
225 | 2 | pub fn new() -> Self { |
226 | 2 | return Self { pipe: None }; |
227 | 2 | } |
228 | | } |
229 | | |
230 | | impl Default for WindowsControlChannelClient { |
231 | 1 | fn default() -> Self { |
232 | 1 | return Self::new(); |
233 | 1 | } |
234 | | } |
235 | | |
236 | | impl ControlChannelClient for WindowsControlChannelClient { |
237 | | type Error = io::Error; |
238 | | |
239 | 1 | async fn connect(&mut self, endpoint: &OsStr) -> Result<(), Self::Error> { |
240 | 1 | let pipe = ClientOptions::new().open(endpoint)?0 ; |
241 | 1 | self.pipe = Some(pipe); |
242 | 1 | return Ok(()); |
243 | 1 | } |
244 | | |
245 | 2 | async fn send(&mut self, bytes: &[u8]) -> Result<(), Self::Error> { |
246 | 2 | let pipe1 = self.pipe.as_mut().ok_or_else(|| {1 |
247 | 1 | return io::Error::other("control channel client is not connected"); |
248 | 1 | })?; |
249 | 1 | return pipe.write_all(bytes).await; |
250 | 2 | } |
251 | | |
252 | 2 | async fn recv(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> { |
253 | 2 | let pipe1 = self.pipe.as_mut().ok_or_else(|| {1 |
254 | 1 | return io::Error::other("control channel client is not connected"); |
255 | 1 | })?; |
256 | 1 | return pipe.read(buf).await; |
257 | 2 | } |
258 | | } |
259 | | |
260 | | /// Window-handle probe backed by [`WindowsApi::get_window_handle_for_process`]. |
261 | | /// |
262 | | /// Holds an [`Arc`] over a [`WindowsApi`] for the same testability reasons |
263 | | /// as [`WindowsProcessSpawner`]. |
264 | | pub struct WindowsWindowHandleProbe<A: WindowsApi = DefaultWindowsApi> { |
265 | | api: Arc<A>, |
266 | | } |
267 | | |
268 | | impl<A: WindowsApi> WindowsWindowHandleProbe<A> { |
269 | | /// Construct a probe around a specific [`WindowsApi`] implementation. |
270 | | /// |
271 | | /// # Arguments |
272 | | /// * `api` - Windows API operations implementation. |
273 | 3 | pub fn new(api: Arc<A>) -> Self { |
274 | 3 | return Self { api }; |
275 | 3 | } |
276 | | } |
277 | | |
278 | | impl Default for WindowsWindowHandleProbe<DefaultWindowsApi> { |
279 | 1 | fn default() -> Self { |
280 | 1 | return Self::new(Arc::new(DefaultWindowsApi)); |
281 | 1 | } |
282 | | } |
283 | | |
284 | | impl<A: WindowsApi + 'static> WindowHandleProbe for WindowsWindowHandleProbe<A> { |
285 | | type Handle = SendHwnd; |
286 | | |
287 | 2 | fn window_handle_for_process(&self, pid: u32) -> Option<Self::Handle> { |
288 | 2 | let handle = self.api.get_window_handle_for_process(pid); |
289 | 2 | if self.api.is_window(handle) { |
290 | 1 | return Some(SendHwnd(handle)); |
291 | 1 | } |
292 | 1 | return None; |
293 | 2 | } |
294 | | } |
295 | | |
296 | | #[cfg(test)] |
297 | | #[path = "tests/test_traits.rs"] |
298 | | mod test_mod; |